#include <assert.h>
#include <errno.h>
#include <limits.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/audioio.h>
#include <unistd.h>

#include "au.h"
#include "adev.h"
#include "common.h"

#define AUDIO "/dev/audio"

enum {
	A_NULL,
	A_ENC,
	A_RATE,
	A_CHAN
};

struct adev {
	int audio;
	int mode;
};

static struct adev	*ad_fdopen(int, int);
static uint32_t		 audio_au_enc(const struct audio_prinfo *);
static int		 au_audio_enc(struct audio_prinfo *, uint32_t);

const struct a_lookup stab[] = {
	{ "enc", A_ENC },
	{ "encoding", A_ENC },
	{ "rate", A_RATE },
	{ "chan", A_CHAN },
	{ "channel", A_CHAN },
	{ "channels", A_CHAN },
	{ NULL, A_NULL }
};

struct adev *
ad_open(const char *dev, int mode)
{
	char path[PATH_MAX];
	struct adev *a;
	int fmode = (mode & A_WRITE) ? O_WRONLY : O_RDONLY;
	int fd;

	if (dev == NULL)
		dev = AUDIO;
	if (snprintf(path, sizeof(path), "%s", dev)
	    > sizeof(path))
		return (NULL);
	if ((fd = open(path, fmode)) >= 0)
		goto end;
	if (snprintf(path, sizeof(path), "/dev/%s", dev)
	    > sizeof(path))
		return (NULL);
	if ((fd = open(path, fmode)) >= 0)
		goto end;
	return (NULL);
end:
	if ((a = ad_fdopen(fd, mode)) == NULL)
		close(fd);
	return (a);
}

struct adev *
ad_fpopen(FILE *fp, int mode)
{

	if (fp == NULL)
		ERR(EINVAL, NULL);
	return (ad_fdopen(fileno(fp), mode));
}

struct adev *
ad_fdopen(int fd, int mode)
{
	struct adev *a;
	uint32_t e;

	if (fd < 0)
		ERR(EINVAL, NULL);
	if ((a = malloc(sizeof(struct adev))) == NULL)
		return (NULL);
	a->audio = fd;
	a->mode = mode;
	/* Set the audio(4) encoding to the closest au(7) encoding */
	if (ad_get(a, "enc", &e) != 1 ||
	    ad_set(a, "enc", e) != 1) {
		ad_close(a);
		return (NULL);
	}
	return (a);
}

void
ad_close(struct adev *a)
{

	if (a == NULL)
		return;
	(void)ioctl(a->audio, AUDIO_DRAIN);
	if (a->audio >= 0)
		close(a->audio);
	free(a);
}

int
ad_limit(struct adev *a)
{

	ERR(ENOSYS, -1);
}

int
ad_get(struct adev *a, const char *s, ...)
{
	audio_info_t info;
	struct audio_prinfo *i;
	va_list ap;
	ssize_t r;
	int n;
	int v;

	if (a == NULL || s == NULL)
		ERR(EINVAL, -1);
	if (ioctl(a->audio, AUDIO_GETINFO, &info) != 0)
		return (-1);
	i = (a->mode & A_WRITE) ? &info.play : &info.record;
	va_start(ap, s);
	for (n = 0; (r = a_lookup(stab, s, &v)) > 0; n++, s += r)
		switch (v) {
		case A_ENC:
			*va_arg(ap, uint32_t *) = audio_au_enc(i);
			break;
		case A_RATE:
			*va_arg(ap, uint32_t *) = i->sample_rate;
			break;
		case A_CHAN:
			*va_arg(ap, uint32_t *) = i->channels;
			break;
		default:
			goto err;
		}
	if (r == 0) {
		va_end(ap);
		return (n);
	}
err:
	va_end(ap);
	ERR(EINVAL, n);
}

int
ad_set(struct adev *a, const char *s, ...)
{
	audio_info_t info;
	struct audio_prinfo *i;
	va_list ap;
	ssize_t r;
	int n;
	int v;

	if (a == NULL || s == NULL)
		ERR(EINVAL, -1);
	if (ioctl(a->audio, AUDIO_GETINFO, &info) != 0)
		return (-1);
	i = (a->mode & A_WRITE) ? &info.play : &info.record;
	va_start(ap, s);
	for (n = 0; (r = a_lookup(stab, s, &v)) > 0; n++, s += r)
		switch (v) {
		case A_ENC:
			if (au_audio_enc(i, va_arg(ap, uint32_t)) != 0)
				goto err;
			break;
		case A_RATE:
			i->sample_rate = va_arg(ap, uint32_t);
			break;
		case A_CHAN:
			i->channels = va_arg(ap, uint32_t);
			break;
		default:
			goto err;
		}
	if (ioctl(a->audio, AUDIO_SETINFO, &info) != 0)
		goto err;
	if (r == 0) {
		va_end(ap);
		return (n);
	}
err:
	va_end(ap);
	ERR(EINVAL, n);
}

ssize_t
ad_read(struct adev *a, void *b, size_t s)
{

	if (a == NULL || a->audio < 0 || b == NULL ||
	    (a->mode & A_WRITE))
		ERR(EINVAL, -1);
	if (s == 0)
		return (0);
	return (read(a->audio, b, s));
}

ssize_t
ad_write(struct adev *a, const void *b, size_t s)
{

	if (a == NULL || a->audio < 0 || b == NULL ||
	    !(a->mode & A_WRITE))
		ERR(EINVAL, -1);
	if (s == 0)
		return (0);
	return (write(a->audio, b, s));
}

uint32_t
audio_au_enc(const struct audio_prinfo *i)
{

	if (i == NULL)
		return (0);
	switch (i->encoding) {
	case AUDIO_ENCODING_ULAW:
		return (AU_ULAW);
	case AUDIO_ENCODING_ALAW:
		return (AU_ALAW);
	case AUDIO_ENCODING_SLINEAR:
		/* FALLTHROUGH */
	case AUDIO_ENCODING_SLINEAR_LE:
		/* FALLTHROUGH */
	case AUDIO_ENCODING_SLINEAR_BE:
		/* FALLTHROUGH */
	case AUDIO_ENCODING_ULINEAR:
		/* FALLTHROUGH */
	case AUDIO_ENCODING_ULINEAR_LE:
		/* FALLTHROUGH */
	case AUDIO_ENCODING_ULINEAR_BE:
		if (i->precision <= 8)
			return (AU_PCM8);
		else if (i->precision <= 16)
			return (AU_PCM16);
		else if (i->precision <= 24)
			return (AU_PCM24);
		else
			return (AU_PCM32);
		break;
	}
	return (0);
}

int
au_audio_enc(struct audio_prinfo *i, uint32_t v)
{

	if (i == NULL)
		return (-1);
	switch (v) {
	case AU_ULAW:
		i->encoding = AUDIO_ENCODING_ULAW;
		i->precision = 8;
		break;
	case AU_PCM8:
	case AU_PCM16:
	case AU_PCM24:
	case AU_PCM32:
		i->encoding = AUDIO_ENCODING_SLINEAR_BE;
		i->precision = au_encsize(v) * 8;
		break;
	case AU_ALAW:
		i->encoding = AUDIO_ENCODING_ALAW;
		i->precision = 8;
		break;
	default:
		return (-1);
	}
	return (0);
}
